/**
 * StrategyCamControl.java
 *
 * Roslan's Remake of MOO in 3D using Ardor3D
 * (C) 2011 Roslan Amir. All rights reserved.
 */

package com.roslan.games.moo3d;

import com.ardor3d.framework.Canvas;
import com.ardor3d.input.MouseState;
import com.ardor3d.input.logical.InputTrigger;
import com.ardor3d.input.logical.LogicalLayer;
import com.ardor3d.input.logical.MouseWheelMovedCondition;
import com.ardor3d.input.logical.TriggerAction;
import com.ardor3d.input.logical.TriggerConditions;
import com.ardor3d.input.logical.TwoInputStates;
import com.ardor3d.math.MathUtils;
import com.ardor3d.math.Vector3;
import com.ardor3d.math.type.ReadOnlyVector3;
import com.ardor3d.renderer.Camera;
import com.ardor3d.scenegraph.Spatial;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

/**
 * Comments go here.
 * 
 * @author Roslan Amir
 * @version 1.0 - Jan 6, 2011
 */
public class StrategyCamControl {

	/**
	 * Comments go here.
	 */
	private enum TargetType {
		Point, Spatial
	}

	/**
	 * Our absolute min/max ascent (pitch) angle, in radians. This is set at 89.95 degrees to prevent the camera's direction
	 * from becoming parallel to the world up vector.
	 */
	public static final double ABSOLUTE_MAXASCENT = 89.95 * MathUtils.DEG_TO_RAD;

	/**
	 * The camera we are modifying.
	 */
	protected Camera _camera;

	/**
	 * 
	 */
	protected Vector3 _worldUpVec = new Vector3(Vector3.UNIT_Y);

	/**
	 * 
	 */
	protected TargetType _targetType = TargetType.Point;

	/**
	 * 
	 */
	protected Vector3 _lookAtPoint = new Vector3();

	/**
     * 
     */
	protected Vector3 _sphereCoords = new Vector3();

	/**
	 * 
	 */
	protected Vector3 _camPosition = new Vector3();

	/**
	 * 
	 */
	protected Spatial _lookAtSpatial;

	/**
     * 
     */
	protected boolean _dirty = true;

	/**
     * 
     */
	protected double _baseDistance = 15.0;

	/**
     * 
     */
	protected double _zoomSpeed = 0.01;

	/**
	 * 
	 */
	protected double _minZoomDistance = 1.0;

	/**
	 * 
	 */
	protected double _maxZoomDistance = 100.0;

	/**
     * 
     */
	protected double _xSpeed = 0.01;

	/**
     * 
     */
	protected double _ySpeed = 0.01;

	/**
     * 
     */
	protected double _minAscent = -ABSOLUTE_MAXASCENT;

	/**
     * 
     */
	protected double _maxAscent = ABSOLUTE_MAXASCENT;

	/**
     * 
     */
	protected InputTrigger _mouseTrigger;

	/**
	 * Construct a new strategy controller initially looking at a specific point in space.
	 * 
	 * @param cam - the camera to control.
	 * @param target - the world location to lock our sights on.
	 */
	public StrategyCamControl(final Camera cam, final ReadOnlyVector3 target) {
		_camera = cam;
		_targetType = TargetType.Point;
		_lookAtPoint.set(target);
	}

	/**
	 * Construct a new strategy controller initially looking at a specific Spatial.
	 * 
	 * @param cam - the camera to control.
	 * @param target - the Spatial whose world location to lock our sights on.
	 */
	public StrategyCamControl(final Camera cam, final Spatial target) {
		_camera = cam;
		_targetType = TargetType.Spatial;
		_lookAtSpatial = target;
	}

	/**
	 * Sets a specific world location for the camera to point at.
	 * 
	 * @param target - the world location to lock our sights on.
	 */
	public void setLookAtPoint(final Vector3 target) {
		_targetType = TargetType.Point;
		if (!target.equals(_lookAtPoint)) {
			_lookAtPoint.set(target);
			_dirty = true;
		}
	}

	/**
	 * Sets a spatial to look at. We'll use the world transform of the spatial, so its transform needs to be up to date.
	 * 
	 * @param spatial - the Spatial whose world location to lock our sights on.
	 */
	public void setLookAtSpatial(final Spatial spatial) {
		_targetType = TargetType.Spatial;
		if (spatial != _lookAtSpatial) { // identity equality
			_lookAtSpatial = spatial;
			_dirty = true;
		}
	}

	/**
	 * Zoom camera in/out from the target point/spatial.
	 * 
	 * @param percent - a value applied to the base distance to determine how far in/out to zoom.
	 */
	public void zoom(final double percent) {
		final double amount = percent * _baseDistance;
		_sphereCoords.setX(MathUtils.clamp(_sphereCoords.getX() + amount, _minZoomDistance, _maxZoomDistance));
		_dirty = true;
	}

	/**
	 * 
	 * @param xDif - a value applied to the azimuth value of our spherical coordinates.
	 * @param yDif - a value applied to the theta value of our spherical coordinates.
	 */
	public void move(final double xDif, final double yDif) {
		final double azimuthAccel = xDif;
		final double thetaAccel = yDif;

		/*
		 * update our master spherical coords, using x and y movement
		 */
		_sphereCoords.setY(MathUtils.moduloPositive(_sphereCoords.getY() - azimuthAccel, MathUtils.TWO_PI));
		_sphereCoords.setZ(MathUtils.clamp(_sphereCoords.getZ() + thetaAccel, _minAscent, _maxAscent));
		_dirty = true;
	}

	/**
	 * Update the position of the Camera controlled by this object.
	 * 
	 * @param time - a delta time, in seconds. Not used currently, but might be useful for doing "ease-in" of camera movements.
	 */
	public void update(final double time) {
		updateTargetPos();

		if (_dirty) {
			if (_worldUpVec.getY() == 1) {
				MathUtils.sphericalToCartesian(_sphereCoords, _camPosition);
			} else if (_worldUpVec.getZ() == 1) {
				MathUtils.sphericalToCartesianZ(_sphereCoords, _camPosition);
			}
			_camera.setLocation(_camPosition.addLocal(_lookAtPoint));
			_camera.lookAt(_lookAtPoint, _worldUpVec);
			_dirty = false;
		}
	}

	/**
	 * Comment for method.
	 * 
	 * @param layer
	 * @param dragOnly
	 */
	public void setupMouseTriggers(final LogicalLayer layer, final boolean dragOnly) {
		/*
		 * Mouse look
		 */
		final Predicate<TwoInputStates> someMouseDown = Predicates.or(TriggerConditions.leftButtonDown(), Predicates.or(TriggerConditions.rightButtonDown(), TriggerConditions.middleButtonDown()));
		final Predicate<TwoInputStates> scrollWheelMoved = new MouseWheelMovedCondition();
		final Predicate<TwoInputStates> dragged = Predicates.and(TriggerConditions.mouseMoved(), someMouseDown);

		final TriggerAction mouseAction = new TriggerAction() {
			/*
			 * TestBox boolean to allow us to ignore first mouse event. First event can wildly vary based on platform.
			 */
			private boolean firstPing = true;

			/*
			 * (non-Javadoc)
			 * 
			 * @see com.ardor3d.input.logical.TriggerAction#perform(com.ardor3d.framework.Canvas,
			 * com.ardor3d.input.logical.TwoInputStates, double)
			 */
			public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
				final MouseState mouse = inputStates.getCurrent().getMouseState();
				if (mouse.getDx() != 0 || mouse.getDy() != 0) {
					if (firstPing) {
						firstPing = false;
					} else {
						move(_xSpeed * mouse.getDx(), _ySpeed * mouse.getDy());
					}
				}

				if (mouse.getDwheel() != 0) {
					zoom(_zoomSpeed * mouse.getDwheel());
				}
			}
		};

		final Predicate<TwoInputStates> predicate = Predicates.or(scrollWheelMoved, dragOnly ? dragged : TriggerConditions.mouseMoved());
		_mouseTrigger = new InputTrigger(predicate, mouseAction);
		layer.registerTrigger(_mouseTrigger);
	}

	/**
	 * Comment for method.
	 */
	private void updateTargetPos() {
		if (_targetType == TargetType.Spatial) {
			final double x = _lookAtPoint.getX();
			final double y = _lookAtPoint.getY();
			final double z = _lookAtPoint.getZ();
			_lookAtSpatial.getWorldTransform().applyForward(Vector3.ZERO, _lookAtPoint);
			if (x != _lookAtPoint.getX() || y != _lookAtPoint.getY() || z != _lookAtPoint.getZ()) {
				_dirty = true;
			}
		}
	}

	/**
	 * Comment for method.
	 * 
	 * @param x - the X coordinate to set
	 * @param y - the Y coordinate to set
	 * @param z - the Z coordinate to set
	 */
	public void setSphereCoords(final double x, final double y, final double z) {
		_sphereCoords.set(x, y, z);
		_dirty = true;
	}

	/**
	 * Comment for method.
	 * 
	 * @param maxAscent
	 */
	public void setMaxAscent(final double maxAscent) {
		_maxAscent = Math.min(maxAscent, ABSOLUTE_MAXASCENT);
		move(0, 0);
	}

	/**
	 * Comment for method.
	 * 
	 * @param minAscent
	 */
	public void setMinAscent(final double minAscent) {
		_minAscent = Math.max(minAscent, -ABSOLUTE_MAXASCENT);
		move(0, 0);
	}

	/**
	 * Comment for method.
	 *
	 * @param maxZoomDistance
	 */
	public void setMaxZoomDistance(final double maxZoomDistance) {
		_maxZoomDistance = maxZoomDistance;
		zoom(0);
	}

	/**
	 * Comment for method.
	 *
	 * @param minZoomDistance
	 */
	public void setMinZoomDistance(final double minZoomDistance) {
		_minZoomDistance = minZoomDistance;
		zoom(0);
	}

}
